﻿/*
   Copyright 2013 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Flower.Directory.Util;
using Flower.Services;
using Flower.Services.Data;

namespace Flower.Directory.Management
{
    public static class DirectoryFind
    {
        private class DirectoryEntryTypesMatcher : IDirectoryEntryVisitor
        {
            private readonly DirectoryEntryTypes _filter;

            public bool Match { get; private set; }

            public DirectoryEntryTypesMatcher(DirectoryEntryTypes filter)
            {
                _filter = filter;
            }

            public void Visit(Assembly assembly)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Assembly);
            }

            public void Visit(Folder folder)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Folder);
            }

            public void Visit(Link link)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Link);
            }

            public void Visit(Message message)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Message);
            }

            public void Visit(Process process)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Process);
            }

            public void Visit(Processor processor)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Processor);
            }

            public void Visit(Role role)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Role);
            }

            public void Visit(ServiceContainer serviceContainer)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.ServiceContainer);
            }

            public void Visit(Set set)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Set);
            }

            public void Visit(State state)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.State);
            }

            public void Visit(Services.Data.Workflow workflow)
            {
                Match = _filter.HasFlag(DirectoryEntryTypes.Workflow);
            }

            public void Visit(DirectoryEntryInfo directoryEntryInfo)
            {
                throw new InvalidOperationException();
            }
        }

        /// <summary>
        /// Searches directory entries by a path wildcard.
        /// </summary>
        /// <param name="dir">The directory.</param>
        /// <param name="what">
        /// The path wildcard.
        /// 
        /// Path wildcard is a path that may contain wildcard symbols '*' for a whole or a part of a
        /// path segments. A wildcard may be at the begining or at the end of a path segment, 
        /// but not in the middle. Also, it may stand for a whole path segment. Double wildcard '**' 
        /// as a path segment matches any number of path segments.
        /// 
        /// For example, '/Sets/Shared/Set*' matches all children of '/Sets/Shared' whose names 
        /// start with 'Set'; '/Sets/Shared/*' matches all children of '/Sets/Shared'; 
        /// '/Sets/Shared/**' matches all descendants of '/Sets/Shared'.
        /// </param>
        /// <param name="entryTypes">The flags specifying entries of which types are to be returned.</param>
        /// <returns>
        /// The collection of found entries or empty collection 
        /// if no entries satisfying the criteria is found.
        /// </returns>
        public static ICollection<DirectoryEntry> Find(this IDirectory dir, string what, DirectoryEntryTypes entryTypes)
        {
            var found = new ConcurrentBag<DirectoryEntry>();

            FindRecursively(dir, found, dir.Get("/"), DirectoryUtil.SplitPath(what), 0);

            var matcher = new DirectoryEntryTypesMatcher(entryTypes);

            return
                found
                    .Where
                    (
                        ent =>
                        {
                            ent.AcceptVisitor(matcher);
                            return matcher.Match;
                        }
                    ).ToList();
        }

        private static void FindRecursivelyByPathEnd(IDirectory dir, ConcurrentBag<DirectoryEntry> found, DirectoryEntry parent, string[] path, int pathStart, List<string> currentPath)
        {
            currentPath.Add(parent.Name);

            if (path[path.Length - 1] == "**")
            {
                found.Add(parent);

                foreach (DirectoryEntry child in EnumerateChildren(dir, parent.Id))
                {
                    FindRecursivelyByPathEnd(dir, found, child, path, pathStart, currentPath);
                }
            }
            else if (dir.CountChildren(parent.Id, DirectoryEntryTypes.All) == 0)
            {
                bool mismatch = false;

                for (int i = 1; i <= path.Length - pathStart; i++)
                {
                    string childName = path[path.Length - i];
                    int currentPathIdx = currentPath.Count - i;

                    if (childName == "**" || currentPathIdx < 0)
                    {
                        break;
                    }
                    else if (childName.StartsWith("*") && !childName.EndsWith("*"))
                    {
                        childName = childName.TrimStart('*');

                        if (!currentPath[currentPathIdx].EndsWith(childName, StringComparison.Ordinal))
                        {
                            mismatch = true;
                            break;
                        }
                    }
                    else if (childName.EndsWith("*") && !childName.StartsWith("*"))
                    {
                        childName = childName.TrimEnd('*');

                        if (!currentPath[currentPathIdx].StartsWith(childName, StringComparison.Ordinal))
                        {
                            mismatch = true;
                            break;
                        }
                    }
                    else if (childName.EndsWith("*") && childName.StartsWith("*"))
                    {
                        childName = childName.Trim('*');

                        if (currentPath[currentPathIdx].IndexOf(childName, 0, StringComparison.Ordinal) < 0)
                        {
                            mismatch = true;
                            break;
                        }
                    }
                    else
                    {
                        if (!currentPath[currentPathIdx].Equals(childName, StringComparison.Ordinal))
                        {
                            mismatch = true;
                            break;
                        }
                    }
                }

                if (!mismatch)
                {
                    found.Add(parent);
                }
            }
            else
            {
                foreach (DirectoryEntry child in EnumerateChildren(dir, parent.Id))
                {
                    FindRecursivelyByPathEnd(dir, found, child, path, pathStart, currentPath);
                }
            }

            currentPath.RemoveAt(currentPath.Count - 1);
        }

        private static void FindRecursively(IDirectory dir, ConcurrentBag<DirectoryEntry> found, DirectoryEntry parent, string[] path, int i)
        {
            if (i == path.Length)
            {
                found.Add(parent);
            }
            else
            {
                string childName = path[i];

                if (childName == "**")
                {
                    foreach (DirectoryEntry child in EnumerateChildren(dir, parent.Id))
                    {
                        FindRecursivelyByPathEnd(dir, found, child, path, i, new List<string>());
                    }
                }
                else if (childName.StartsWith("*") && !childName.EndsWith("*"))
                {
                    childName = childName.TrimStart('*');
                    foreach (DirectoryEntry child in EnumerateChildren(dir, parent.Id))
                    {
                        if (child.Name.EndsWith(childName, StringComparison.Ordinal))
                        {
                            FindRecursively(dir, found, child, path, i + 1);
                        }
                    }
                }
                else if (childName.EndsWith("*") && !childName.StartsWith("*"))
                {
                    childName = childName.TrimEnd('*');
                    foreach (DirectoryEntry child in EnumerateChildren(dir, parent.Id))
                    {
                        if (child.Name.StartsWith(childName, StringComparison.Ordinal))
                        {
                            FindRecursively(dir, found, child, path, i + 1);
                        }
                    }
                }
                else if (childName.EndsWith("*") && childName.StartsWith("*"))
                {
                    childName = childName.Trim('*');
                    foreach (DirectoryEntry child in EnumerateChildren(dir, parent.Id))
                    {
                        if (child.Name.IndexOf(childName, 0, StringComparison.Ordinal) >= 0)
                        {
                            FindRecursively(dir, found, child, path, i + 1);
                        }
                    }
                }
                else
                {
                    foreach (DirectoryEntry child in EnumerateChildren(dir, parent.Id))
                    {
                        if (child.Name.Equals(childName, StringComparison.Ordinal))
                        {
                            FindRecursively(dir, found, child, path, i + 1);
                            return;
                        }
                    }
                }
            }
        }

        private static IEnumerable<DirectoryEntry> EnumerateChildren(IDirectory dir, string parent)
        {
            const int batchSize = 100;

            int offset = 0;
            int limit = batchSize;
            IList<DirectoryEntry> batch;

            do
            {
                batch = dir.GetChildren(parent, DirectoryEntryTypes.All, SortProperty.Default, SortOrder.Asc, offset, limit);

                foreach (DirectoryEntry directoryEntry in batch)
                {
                    yield return directoryEntry;
                }

                offset = limit;
                limit += batchSize;
            }
            while (batch.Count > 0);
        }
    }
}
